package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.IntPair;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;

import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.mutable.MutableInt;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.Range;

public class PeakSlopes implements Iterable<IntPair> {

  // Always in strictly increasing order.
  private final IntPair[] startSlopeAndPeakIndices;

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof PeakSlopes)) {
      return false;
    }
    PeakSlopes other = (PeakSlopes) obj;
    return Arrays.equals(startSlopeAndPeakIndices, other.startSlopeAndPeakIndices);
  }

  private PeakSlopes(IntPair[] startSlopeAndPeakIndices) {
    checkIncreasing(startSlopeAndPeakIndices);
    this.startSlopeAndPeakIndices = startSlopeAndPeakIndices;
  }

  private static void checkIncreasing(IntPair[] startSlopeAndPeakIndices) {
    for (int i = 0; i < startSlopeAndPeakIndices.length; ++i) {
      IntPair ip = startSlopeAndPeakIndices[i];
      // see PicoDataAnalyser.startPeakSlopes
      Preconditions.checkArgument(ip.x <= ip.y);
      Preconditions.checkArgument(ip.x > (i == 0 ? -1 : startSlopeAndPeakIndices[i - 1].y));
    }
  }

  public static PeakSlopes of(int[] startSlopeIndices) {
    int n = CommonThings.countDifferentFrom(startSlopeIndices, -1);
    IntPair[] startSlopeAndPeakIndex = new IntPair[n];
    int j = 0;
    for (int i = 0; i < startSlopeIndices.length; ++i) {
      if (startSlopeIndices[i] != -1) {
        startSlopeAndPeakIndex[j++] = IntPair.of(startSlopeIndices[i], i);
      }
    }
    return new PeakSlopes(startSlopeAndPeakIndex);
  }

  public int[] toStartSlopeIndices(int n) {
    Preconditions.checkArgument(n >= 0);
    int[] a = new int[n];
    Arrays.fill(a, -1);
    if (startSlopeAndPeakIndices.length != 0) {
      Preconditions.checkArgument(startSlopeAndPeakIndices[startSlopeAndPeakIndices.length - 1].y < n);
      for (IntPair ip : startSlopeAndPeakIndices) {
        a[ip.y] = ip.x;
      }
    }
    return a;
  }

  public String repr() {
    return "PeakSlopes([" + IntPair.repr(this.startSlopeAndPeakIndices, "") + "])";
  }

  private static String regex = "^PeakSlopes\\(\\[([^]]*)\\]\\)$";
  private static final Pattern pattern = Pattern.compile(regex);
  public static final PeakSlopes EMPTY = PeakSlopes.of(ArrayUtils.EMPTY_INT_ARRAY);

  public static PeakSlopes parseRepr(String s) {
    Matcher matcher = pattern.matcher(s);
    boolean matches = matcher.matches();
    final String v = matches ? matcher.group(1) : s;
    // If there is no match, perhaps it's the old format.
    return new PeakSlopes(IntPair.eval(v, ""));
  }

  public static List<PeakSlopes> parseListRepr(String s) {
    if (!s.startsWith("[")) {
      /*
       * Either the old or the new format. For example, Refolding still writes
       * only one PeakSlopes in the file, though in the new format while
       * BatchApproachRetractionAnalysis expects a list instead.
       */
      return ImmutableList.of(parseRepr(s));
    } else {
      String[] s2 = CommonThings.parseArrayRepr(s, "PeakSlopes");
      PeakSlopes[] peaks = new PeakSlopes[s2.length];
      for (int i = peaks.length; --i >= 0;) {
        String r = (i == 0 ? "" : "PeakSlopes") + s2[i];
        peaks[i] = parseRepr(r);
      }
      return Collections.unmodifiableList(Arrays.asList(peaks));
    }
  }

  public static String repr(List<PeakSlopes> a) {
    return '[' + StringUtils.join(a, ", ") + ']';
  }

  @Override
  public String toString() {
    return repr();
  }

  @Override
  public Iterator<IntPair> iterator() {
    return Iterators.unmodifiableIterator(Arrays.asList(startSlopeAndPeakIndices).iterator());
  }

  public int size() {
    return startSlopeAndPeakIndices.length;
  }

  public IntPair get(int i) {
    return startSlopeAndPeakIndices[i];
  }

  public PeakSlopes withoutPeakSlope(int i) {
    IntPair[] ips = Arrays.copyOf(startSlopeAndPeakIndices, startSlopeAndPeakIndices.length - 1);
    System.arraycopy(startSlopeAndPeakIndices, i + 1, ips, i, startSlopeAndPeakIndices.length - i
      - 1);
    return new PeakSlopes(ips);
  }

  public int peakIndex(int index) {
    for (int i = 0; i < size(); ++i) {
      if (startSlopeAndPeakIndices[i].x <= index & index <= startSlopeAndPeakIndices[i].y) {
        return i;
      }
    }
    return -1;
  }

  public int[] overlapping(int a, int b) {
    IntList l = new IntArrayList();
    Range<Integer> key = Range.closed(a, b);
    for (int i = 0; i < size(); ++i) {
      Range<Integer> target = Range.closed(
        startSlopeAndPeakIndices[i].x, startSlopeAndPeakIndices[i].y);
      if (key.isConnected(target)) {
        boolean added = l.add(i);
        assert added : l;
      }
    }
    return l.toIntArray();
  }

  public PeakSlopes withAddedPeakSlope(int a, int b, MutableInt insertionPoint) {
    IntPair ip = IntPair.of(a, b);
    int bs = Arrays.binarySearch(startSlopeAndPeakIndices, ip);
    Preconditions.checkArgument(bs < 0);
    int pos = -bs - 1;
    IntPair[] ips = Arrays.copyOf(startSlopeAndPeakIndices, size() + 1);
    ips[pos] = ip;
    System.arraycopy(startSlopeAndPeakIndices, pos, ips, pos + 1, startSlopeAndPeakIndices.length
      - pos);
    insertionPoint.setValue(pos);
    return new PeakSlopes(ips);
  }

  public PeakSlopes[] deepCopies(int n) {
    PeakSlopes[] a = new PeakSlopes[n];
    for (int i = n; --i >= 0;) {
      a[i] = deepClone();
    }
    return a;
  }

  private PeakSlopes deepClone() {
    return new PeakSlopes(startSlopeAndPeakIndices.clone());
  }

  public static PeakSlopes[] deepCopies(PeakSlopes prototype, int n) {
    return prototype == null ? new PeakSlopes[n] : prototype.deepCopies(n);
  }
}
